//NoteToolbox Revision 1

import java.lang.Math.*;
import java.util.*;
public class NoteToolbox {

  //In the calculation of frequencies, a constant of the 12th root of 2
  //is used as the frequencies correspond to twelve-tone equal temperament.
  static final double equalTempered12 = Math.pow(2.0, (1.0/12.0));
  
  //A chromatically ascending scale from C to B in twelve-tone equal temperament
  //This array of strings is used in the methods of: mapPitch, getEnharmonic, cyclicPosition, interval, and getFreq.

  //Two-character entries, such as CD, represent the pitch class of the first character
  //sharp and the pitch class of the second character flat.  
  //In the case of CD, it represents C# and Db.
  static final String[] coreEnharmonic12 = {"C", "CD", "D", "DE", "E", "F", "FG", "G", "GA", "A", "AB", "B"};
  
  
  
  /* This method takes a given pitch (sans octave) and maps the pitch class to one of the
   * twelve core enharmonic pitches that establish twelve-tone equal temperament. */
  
  /* For example, the pitches of: C, Dbb, and B# are mapped to the core enharmonic of C. */
  /* Accidentals are written as such: Flat (b), Sharp (#), Double-flat (bb), Double-sharp (x) */
  
 
  public static String mapPitch(String pitch) {
    
    //Prior to any mapping, the pitch is checked for validity.
    //If it's not a valid pitch, then an error is raised.
    boolean goodData = validPitch(pitch + "0");
    if (!goodData) {
      throw new IllegalArgumentException("Invalid Note String - Cannot Map Pitch");
    } else {
    
    int semitoneShift = 0;  //How many semitones a given pitch is raised or lower (from -2 to 2)
    int baseIndex = 0; //Index position of the accidental-less pitch in the coreEnharmonic12 array
    int newIndex = 0; //Index position of the mapped pitch
    
    char pitchLetter = pitch.charAt(0);
    pitchLetter = Character.toUpperCase(pitchLetter);
    
    switch (pitchLetter) {
      case 'C': baseIndex = 0;
      break;
      case 'D': baseIndex = 2;
      break;
      case 'E': baseIndex = 4;
      break;
      case 'F': baseIndex = 5;
      break;
      case 'G': baseIndex = 7;
      break;
      case 'A': baseIndex = 9;
      break;
      case 'B': baseIndex = 10;
      break;
    }
    
    if (pitch.length() > 1) {
      
    //Determine the accidental attached to the given pitch and the corresponding semitone shift associated with it
    
    if (pitch.endsWith("bb")) {
      semitoneShift = -2;
    } else if (pitch.endsWith("b")) {
      semitoneShift = -1;
    } else if (pitch.endsWith("#")) {
      semitoneShift = 1;
    } else {
      semitoneShift = 2;
    }
    
    }
    
    newIndex = baseIndex + semitoneShift; //The circular index position of mapped note
    return coreEnharmonic12[cirInd(newIndex, 12)];
    
    }

  }
  
  /* This method takes two Note objects, Note A and Note B, and computes the semitone interval
  /* from Note A to Note B.   That is, interval = NoteB - NoteA.
  
  /* Note A can be understood as the note of reference (e.g., A4 in the calculation of frequencies in 12-TET). */
  
  public static int interval(NSNote A, NSNote B) {
    
    int octaveA = A.getOctave();
    int octaveB = B.getOctave();
    int semitoneInterval = 0;
    
    //How many whole octaves, of twelve semitones each, separate the two notes.
    //WholeOctDiff is a value of distance (i.e. greater or equal to 0)
    int wholeOctDiff = 12 * (Math.abs(octaveB - octaveA) - 1); 
    
    if (octaveA == octaveB) { //Interval can be either sign
 semitoneInterval = cyclicPosition(B, "chr-a") - cyclicPosition(A, "chr-a");
 return semitoneInterval;
    } else if (octaveA < octaveB) { //Positive Interval
 semitoneInterval = (12 - cyclicPosition(A, "chr-a")) + wholeOctDiff + cyclicPosition(B, "chr-a");
    } else { //OctaveA > OctaveB - Negative Interval
 semitoneInterval = -((12 - cyclicPosition(B, "chr-a")) + wholeOctDiff + cyclicPosition(A, "chr-a"));
    }
    
    return semitoneInterval;
  
  }
  
  //Given a Note 'myNote', this method will find all the possible enharmonics
  //to the given mapped pitch.   Essentially, this is the inverse of the mapPitch() method.
  
  public static String[] getEnharmonic(NSNote myNote) {
    
    ArrayList<String> enharmonicNotes = new ArrayList<String>();
    String pitchClass = "";
    int corePosition = cyclicPosition(myNote, "chr-a") - 1;  
    //The mapped pitch is a two-character entry, then it represents the pitch class of the first-character sharp
    //and the pitch class of the second-character flat.   E.g.  CD = C# and Db,   GA = G# and Ab
    
    if ((myNote.getMappedPitch()).length() == 2) {
      enharmonicNotes.add((myNote.getMappedPitch()).substring(0,1) + "#");
      enharmonicNotes.add((myNote.getMappedPitch()).substring(1,1) + "b");
    } else {
      enharmonicNotes.add(myNote.getMappedPitch());
    }
    
    //In the following procedure to retrieve flats, sharps, and their doubles, if an inverse mapped pitch class
    //is a two-character entry, then it cannot be sharpened or flattened as it is already modified.
    //That is, A#b is invalid.
    
    //Test for Double-Sharp Enharmonic
    
    if (coreEnharmonic12[cirInd((corePosition-2), 12)].length() == 1) {
      enharmonicNotes.add(coreEnharmonic12[cirInd((corePosition-2), 12)] + "x");
    }
    
    //Test for Sharp Enharmonic
    
    if (coreEnharmonic12[cirInd((corePosition-1), 12)].length() == 1) {
      enharmonicNotes.add(coreEnharmonic12[cirInd((corePosition-1), 12)] + "#");
    }
    
    //Test for Flat Enharmonic
    
    if (coreEnharmonic12[cirInd((corePosition+1), 12)].length() == 1) {
      enharmonicNotes.add(coreEnharmonic12[cirInd((corePosition-2), 12)] + "b");
    }
    
    //Test for Double-Flat Enharmonic
    
    if (coreEnharmonic12[cirInd((corePosition-2), 12)].length() == 1) {
      enharmonicNotes.add(coreEnharmonic12[cirInd((corePosition-2), 12)] + "bb");
    }
    
    String[] noteList = new String[enharmonicNotes.size()];
    
    //Convert from Java ArrayList to String Array for easier MATLAB processing
    
    for (int i = 0; i < enharmonicNotes.size(); i++) {
      noteList[i] = enharmonicNotes.get(i);
    }
    
    return noteList;
  }
  
  /* This method takes as input a circular index position (i.e. a signed index position that can be greater
   * the defined length of the linear array) and returns an linear index position */
  
  public static int cirInd(int pos, int length) {
    
    int linearIndex = 0;
    int maxIndex = length - 1;
    
    if ((pos % maxIndex) >= 0) {
      linearIndex = pos % maxIndex;
    } else {
      linearIndex = length + (pos % maxIndex);
    }
   
    return linearIndex;
  }
  
  /* This method takes as input a Note object, a specific cycle 
   * (i.e., ascending chromatic, descending chromatic, fourths, and fifths), 
   * and returns the cyclic position of the pitch class in the given cycle. */
  
  public static int cyclicPosition(NSNote myNote, String cycleType) {
    
    /* Accepted inputs for cycleType string (Case-insensitive):  
     * 'chr-a' --> Chromatic Ascending  
     * 'chr-d' --> Chromatic Descending
     * 'fourths' --> Circle of Fourths
     * 'fifths' --> Circle of Fifths */
    
    String mappedPitch = myNote.getMappedPitch();
    int position = 0;
    String[] musicalCycle;
    
    if (cycleType.equalsIgnoreCase("chr-a")) {
      musicalCycle = coreEnharmonic12;
    } else if (cycleType.equalsIgnoreCase("chr-d")) {
      musicalCycle = new String[]{"C", "B", "AB", "A", "GA", "G", "FG", "F", "E", "DE", "D", "CD"};
    } else if (cycleType.equalsIgnoreCase("fifths")) {
      musicalCycle = new String[]{"C", "G", "D", "A", "E", "B", "FG", "CD", "GA", "DE", "AB", "F"};
    } else {
      musicalCycle = new String[]{"C", "F", "AB", "DE", "GA", "CD", "FG", "B", "E", "A", "D", "G"};
    }
    
    //Note: Array indexing starts at 0, while the cyclic position starts from 1, so the index is shifted by +1
    //to compute the cyclic position.
    
    for (int i = 0; i < 12; i++) {
      if (mappedPitch.equalsIgnoreCase(musicalCycle[i])) {
        position = i + 1;
        return i;
      }
    }

   return 0; 
  }
  
  /* This methods takes a given Note object and finds its corresponding frequency in 12-TET with
  /* A4 (440 Hz) as the note of reference.  */
  
  public static double calculateFreq(NSNote myNote) {
    //In the calculation of frequencies, A4 is used as the reference
    //point that all other notes refer to.
    NSNote A440 = new NSNote("A4", 440.0);
    
    int distanceFromA4 = interval(A440, myNote);
    double frequency = 440 * Math.pow(equalTempered12, (double) distanceFromA4);
    
    A440 = null; //Clear out object
    return frequency;
  }
  
  
  /* This methods takes as input a pitch (that can feature an accidental) and determines whether
   * it is a valid pitch or not. */
  
  public static boolean validPitch(String pitch) {
    //Regular expressions checks to see if pitch is constructed as: 
    //[Pitch Class] + [Optional Accidental] + [Optional Octave]
    
    boolean regexTest = pitch.matches("^[a-gA-G](#|x|b|bb)?(-)?\\d+");
    return regexTest;
    
  }
  
  
  public static String test() {
    //Test for MATLAB interaction - to see if MATLAB has accessed the NoteSpace files
    return "NoteSPACE detected";
  }
  
}